【字符串匹配】KMP算法之道

【字符串匹配】KMP算法之道

修订于2012-06-18,心急的读者可以着重看“有趣的字符串匹配提示”,这个例子看懂了,KMP也就差不多了。

都是字符串匹配

【字符串匹配】最为朴素的匹配和Rabin-Karp算法

【字符串匹配】有限自动机进行字符串匹配

【字符串匹配】KMP算法之道(即本篇)

《导论》是本好书啊,算法描述思路很清晰,力荐!

闲话

上午算法考试的时候,感觉OK,前一两星期幸好把图算法都吃透了一遍,复习的时候节省了时间:)。前一半考题不理解背书的都可以,有几题没记过,不靠谱地照着理解写下来。最后的吹水题让我想起了之前的比赛,有一题是曹老师给的实验题,刚好比赛上出现了,而且相似度极高。要是高考,曹老师可就红了:)。这也让我捡了便宜。

我们校区2012的招生计划出来了,结果我们校区悲催到只招30个法语本科生,也就是说2012的本科孩子只有30人。不知道法语的怎么看,但对这个校区的未来,我是看不到什么希望。“坑爹啊...”

有趣的字符串匹配“提示”

对于T=abacabab,P=abab,从T的第一个字符开始匹配:

  a b a a b a b
  a b a b      
第一次匹配 1 2 3 0      

可以看到,第四个字符已经匹配失败了。此时如果采用最朴素的算法,也就是重新从第二字符开始匹配(不画表了)。

KMP是这样做的:既然上面第四个字符已经匹配失败了,那么可以试着从已经匹配成功的前三个字符(即上面的“aba”)找到既是“aba”的后缀又是“aba”的前缀的字串,要求是此字串长度应该是所有满足条件中最大的,暂且记为π(“aba”)。很显然,π(“aba”)=1,因为

a b a 
      a b a

因此猜测从第三个字符开始匹配可能会成功(其实应该是从第四个字符开始匹配,因为π(“aba”)=1已经暗示第三个字符“a”匹配成功)(猜测,只是猜测而已)

  a b a a b a b
第一次匹配 a b a b      
第二次匹配     a
a b  

好吧,结果是不成功,因为出现了T中的第四个字符匹配失败的情况。不过可以发现,KMP算法没有像朴素算法那样,从T的第二个字符开始匹配,转而从T的第三个字符开始匹配,那为什么不从第二个字符开始匹配呢,因为从T的第三个字符开始匹配才有可能是成功的。如果你认为(或者说你有足够的证据证明)从第二字符开始匹配会成功,那么上面找“既前缀又后缀”的结果:

b a 
   a b a

即π(“aba”)=2,很明显不是。

好吧,这里不成功,用上面一样的策略,此时π(“a”)=0。因为

a  
   a

这逼着从T的第四个字符开始匹配,也是不成功:

  a b a a b a b
第一次匹配     a b      
第二次匹配      
b a b

于是匹配成功。你将看到KMP也是这么做的,关键是如何计算上面的说的“既前缀又后缀”的结果——其实就是帮助匹配的辅助表。

KMP算法之道

问题定义:

字符串匹配问题:T=“www.daoluan.net”,P=“daoluan”,问P是否在T中出现?答:是。

之前遇到的字符串匹配算法效率不是很看好,有限自动机之于最为朴素的穷举法有一定的提高,但是初始化过程仍不乐观,总体效率不高。奇葩的是,KMP算法初始化和匹配过程分别可以达到O(n)和O(m),实在是神奇。本篇文章目的就是吃透KMP。

纵观KMP,它无非就基于三个核心的结论,吃透这个三个结论,将KMP踩在脚下。

KMP和有限自动机字符串匹配一样,借助了一个辅助一维表,但KMP的辅助表计算时间在O(m)内。这个辅助表是关于匹配内容P的前缀表。

在提及这些结论之前,先允许我啰嗦一下:

对于字串P,(k)P表示长度k的P的前缀;P(k)表示长度为k的P的后缀。比如P=abcdef,(3)P=abc,P(3)=def。

π(q)表示P的前缀(q)P的最长后缀(k)P的长度(也就是k要取最大值)。比如:P=ababababca,π(8)即(8)P=abababab的最长后缀(k)P的长度,k最大为6,因为

abababab|ca 
ababab|abca

∴π(8)=6。

上述的辅助表中元素i即为π(i),比如:

匹配内容P=“daodaodaodaoluan”,那么关于P的前缀表即为: 
P d a o d a o d a o d a o l  u a n  
π 0 0 0 1 2 3 4 5 6 7 8 9 0 0 0 0

π(q)有了定义,π*(q)可以有。通常加“*”表示所有,在这里也是。π*(q)是一个集合,它的所有成员可以迭代求出:

π*(q)={k|(k)P是(q)P的后缀且k<q}

={π1(q),π2(q),π3(q),π4(q)....},其中πn(q)=π[πn-1(q)]。

比如:同样对P=ababababca,求π*(8), 有下图结果(来自算法导论):

image

所以π*(8)={0,2,4,6}。对于其他的q值也是这样计算。

假设已经得到了关于P的辅助表,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kmp()
     m = strlen (P)
     n = strlen (T)
     π[m]
     kmptab(π)    //预处理辅助表
     q = -1
     for i=[0,n)
         while q>0 && P[q+1]!=T[i]
             q = π[q]
         if P[q+1]==T[i]
             q = q+1
         if q = m
             //    找到啦
         q = π[q]    //继续剩余T的寻找

其中π[m]为辅助表。如果P[q]==T[i]能连续成立m次,那么可以找到T中的P。所以如果有辅助表的存在,匹配过程还是很容易理解的。

亮出三把秒杀KMP的“宝剑”,他们主要是用来计算辅助表的:

  1. π*(q)={k|(k)P是(q)P的后缀且k<q}。
  2. 如果π(q)>0,那么π(q)-1∈π*(q-1)。
  3. 若定义Eq-1={k|k<q-1且(k)P是(q-1)P的后缀且P[k+1]=P[q]}。那么有: 
    π(q)=0                           Eq-1是空集 
            =1+max(k∈Eq-1)   Eq-1非空 

第一个结论可以通过“包含反包含”得出,用“显然”描述不为过吧:)。

第二个:

∵π(q)=t,那么t<q,所以π(q)-1=t-1<q-1;

∵π(q)=t,∴(t)P是(q)P的后缀,(去掉(t)P和(q)P的最后一个字符)那么(t-1)P同样也是(q-1)P的后缀;

根据结论一,得到t-1∈π*(q-1),π(q)-1∈π*(q-1)总能成立。

第三个:

可以证明它,但是个人觉得图例更有助于我们理解,下图对第三个结论的理解很有帮助的,里面涉及了上面提到的定义,比如π等,个人认为最好先懂了定义再来看,否则skip吧。 
image

上面的图中,前缀π(i-1)不断缩短,知道找到第i个字符和第【π(i-1)+1】个字符相等为止。

上面的基础就是为计算辅助表的。有了上面的结论:

?
1
2
3
4
5
6
7
8
9
10
11
kmptab(π)
     m = strlen (P)
     q = 0
     π[0] = 0
     for i=[1,m)
         while q>0 && P[q]!=P[i]
             q = π[q]    //    这里领悟到没有,跟第一个结论很有暧昧;同时,它跟第三个结论也有很大的渊源。
 
         if P[q]=P[i]
             q = q + 1    //    与上图中的做法一致
         π[i] = q

实在是太短了。

KMP的复杂度

KMP的复杂度一时我也说不清楚,借助了算法导论和Matrix67的手笔才略有领悟。KMP用到了平摊分析。就上面的kmptab(π)函数,从q的值来说,q = π[q]操作只会使的q越来越小,但总能q>0,因为q = π[q]是根据辅助表来得到的,而辅助表中最小为0。P[q]=P[i]条件成立,能使得q+1,也就是说q只有在这里才增加。最坏的情况,q被增加m-1次。所以while循环内的操作最多被执行m-1次的。平摊一下就是O(1)了。所以加上for循环,kmptab的复杂度为O(m)。 
kmp主程序也是用这种平摊分析方法。

补充:KMP匹配过程中利用辅助表跳过了无效的检查,直接将检查过程跳转到将来可能成功匹配的字符上。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种用于字符串匹配算法,它的核心思想是利用部分匹配表来避免不必要的比较。下面是KMP算法的原理和实现步骤: 1. 部分匹配表的计算: - 部分匹配值是指字符串的前缀和后缀的最长公共部分的长度。 - 部分匹配表是一个数组,记录了每个位置的部分匹配值。 - 部分匹配表的计算可以通过动态规划的方式进行,具体步骤如下: - 初始化部分匹配表的第一个元素为0。 - 从第二个元素开始,依次计算每个位置的部分匹配值: - 如果当前位置的字符与前一个位置的部分匹配值对应的字符相等,则部分匹配值加1。 - 如果不相等,则需要回溯到前一个位置的部分匹配值对应的字符的部分匹配值,继续比较。 - 在主串中从左到右依次比较字符,同时在模式串中根据部分匹配表进行跳跃。 - 如果当前字符匹配成功,则继续比较下一个字符。 - 如果当前字符匹配失败,则根据部分匹配表找到模式串中需要跳跃的位置,继续比较。 下面是一个使用KMP算法进行字符串匹配的示例代码: ```python def kmp_search(text, pattern): n = len(text) m = len(pattern) next = get_next(pattern) i = 0 j = 0 while i < n and j < m: if j == -1 or text[i] == pattern[j]: i += 1 j += 1 else: j = next[j] if j == m: return i - j else: return -1 def get_next(pattern): m = len(pattern) next = [-1] * m i = 0 j = -1 while i < m - 1: if j == -1 or pattern[i] == pattern[j]: i += 1 j += 1 next[i] = j else: j = next[j] return next ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值